package org.jdesktop.swingx.plaf.basic;

import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.logging.Logger;

import javax.swing.AbstractButton;
import javax.swing.AbstractSpinnerModel;
import javax.swing.Action;
import javax.swing.BorderFactory;
import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.JLabel;
import javax.swing.JSpinner;
import javax.swing.JSpinner.DefaultEditor;
import javax.swing.JSpinner.NumberEditor;
import javax.swing.SpinnerModel;
import javax.swing.UIManager;

import org.jdesktop.swingx.JXHyperlink;
import org.jdesktop.swingx.JXMonthView;
import org.jdesktop.swingx.JXPanel;
import org.jdesktop.swingx.renderer.FormatStringValue;

/**
 * Custom CalendarHeaderHandler which supports year-wise navigation.
 * <p>
 * 
 * The custom component used as header component of this implementation contains
 * month-navigation buttons, a label with localized month text and a spinner for
 * .. well ... spinning the years. There is minimal configuration control via
 * the UIManager:
 * 
 * <ul>
 * <li>control the position of the nextMonth button: the default is at the
 * trailing edge of the header. Option is to insert it directly after the month
 * text, to enable set a Boolean.TRUE as value for key
 * <code>ARROWS_SURROUNDS_MONTH</code>.
 * <li>control the focusability of the spinner's text field: the default is
 * false. To enable set a Boolean.TRUE as value for key
 * <code>FOCUSABLE_SPINNER_TEXT</code>.
 * </ul>
 * 
 * <b>Note</b>: this header is <b>not</b> used by default. To make it the
 * per-application default register it with the UIManager, like
 * 
 * <pre>
 * <code>
 * UIManager.put(CalendarHeaderHandler.uiControllerID, 
 *      "org.jdesktop.swingx.plaf.basic.SpinningCalendarHeaderHandler");
 * </code>
 * </pre>
 * 
 * PENDING JW: implement and bind actions for keyboard navigation. These are
 * potentially different from navigation by mouse: need to move the selection
 * along with the scrolling?
 * 
 */
public class SpinningCalendarHeaderHandler extends CalendarHeaderHandler {

	/**
	 * Key for use in UIManager to control the position of the nextMonth arrow.
	 */
	public static final String ARROWS_SURROUND_MONTH = "SpinningCalendarHeader.arrowsSurroundMonth";

	/**
	 * Key for use in UIManager to control the focusable property of the year
	 * spinner.
	 */
	public static final String FOCUSABLE_SPINNER_TEXT = "SpinningCalendarHeader.focusableSpinnerText";

	@SuppressWarnings("unused")
	private static final Logger LOG = Logger.getLogger(SpinningCalendarHeaderHandler.class.getName());

	/** the spinner model for year-wise navigation. */
	private SpinnerModel yearSpinnerModel;

	/** listener for property changes of the JXMonthView. */
	private PropertyChangeListener monthPropertyListener;

	/** converter for month text. */
	private FormatStringValue monthStringValue;

	// ----------------- public/protected overrides to manage custom
	// creation/config

	/**
	 * {@inheritDoc}
	 * <p>
	 * 
	 * Overridden to configure header specifics component after calling super.
	 */
	@Override
	public void install(JXMonthView monthView) {
		super.install(monthView);
		getHeaderComponent().setActions(monthView.getActionMap().get("previousMonth"),
				monthView.getActionMap().get("nextMonth"), getYearSpinnerModel());
		componentOrientationChanged();
		monthStringBackgroundChanged();
		fontChanged();
		localeChanged();
	}

	/**
	 * {@inheritDoc}
	 * <p>
	 * 
	 * Overridden to cleanup the specifics before calling super.
	 */
	@Override
	public void uninstall(JXMonthView monthView) {
		getHeaderComponent().setActions(null, null, null);
		getHeaderComponent().setMonthText("");
		super.uninstall(monthView);
	}

	/**
	 * {@inheritDoc}
	 * <p>
	 * 
	 * Convenience override to the type created.
	 */
	@Override
	public SpinningCalendarHeader getHeaderComponent() {
		return (SpinningCalendarHeader) super.getHeaderComponent();
	}

	/**
	 * {@inheritDoc}
	 * <p>
	 * 
	 * Implemented to create and configure the custom header component.
	 */
	@Override
	protected SpinningCalendarHeader createCalendarHeader() {
		SpinningCalendarHeader header = new SpinningCalendarHeader();
		if (Boolean.TRUE.equals(UIManager.getBoolean(FOCUSABLE_SPINNER_TEXT))) {
			header.setSpinnerFocusable(true);
		}
		if (Boolean.TRUE.equals(UIManager.getBoolean(ARROWS_SURROUND_MONTH))) {
			header.setArrowsSurroundMonth(true);
		}
		return header;
	}

	/**
	 * {@inheritDoc}
	 * <p>
	 */
	@Override
	protected void installListeners() {
		super.installListeners();
		monthView.addPropertyChangeListener(getPropertyChangeListener());
	}

	/**
	 * {@inheritDoc}
	 * <p>
	 */
	@Override
	protected void uninstallListeners() {
		monthView.removePropertyChangeListener(getPropertyChangeListener());
		super.uninstallListeners();
	}

	// ---------------- listening/update triggered by changes of the JXMonthView

	/**
	 * Updates the formatter of the month text to the JXMonthView's Locale.
	 */
	protected void updateFormatters() {
		SimpleDateFormat monthNameFormat = (SimpleDateFormat) DateFormat.getDateInstance(DateFormat.SHORT,
				monthView.getLocale());
		monthNameFormat.applyPattern("MMMM");
		monthStringValue = new FormatStringValue(monthNameFormat);
	}

	/**
	 * Updates internal state to monthView's firstDisplayedDay.
	 */
	protected void firstDisplayedDayChanged() {
		((YearSpinnerModel) getYearSpinnerModel()).fireStateChanged();
		getHeaderComponent().setMonthText(monthStringValue.getString(monthView.getFirstDisplayedDay()));
	}

	/**
	 * Updates internal state to monthView's locale.
	 */
	protected void localeChanged() {
		updateFormatters();
		firstDisplayedDayChanged();
	}

	/**
	 * Returns the property change listener for use on the monthView. This is
	 * lazyly created if not yet done. This implementation listens to changes of
	 * firstDisplayedDay and locale property and updates internal state
	 * accordingly.
	 * 
	 * @return the property change listener for the monthView, never null.
	 */
	private PropertyChangeListener getPropertyChangeListener() {
		if (monthPropertyListener == null) {
			monthPropertyListener = new PropertyChangeListener() {

				@Override
				public void propertyChange(PropertyChangeEvent evt) {
					if ("firstDisplayedDay".equals(evt.getPropertyName())) {
						firstDisplayedDayChanged();
					} else if ("locale".equals(evt.getPropertyName())) {
						localeChanged();
					}

				}

			};
		}
		return monthPropertyListener;
	}

	// ---------------------- methods to back to Spinner model

	/**
	 * Returns the current year of the monthView. Callback for spinner model.
	 * 
	 * return the current year of the monthView.
	 */
	private int getYear() {
		Calendar cal = monthView.getCalendar();
		return cal.get(Calendar.YEAR);
	}

	/**
	 * Returns the previous year of the monthView. Callback for spinner model.
	 * <p>
	 * 
	 * PENDING JW: check against lower bound.
	 * 
	 * return the previous year of the monthView.
	 */
	private int getPreviousYear() {
		Calendar cal = monthView.getCalendar();
		cal.add(Calendar.YEAR, -1);
		return cal.get(Calendar.YEAR);
	}

	/**
	 * Returns the next year of the monthView. Callback for spinner model.
	 * <p>
	 * 
	 * PENDING JW: check against upper bound.
	 * 
	 * return the next year of the monthView.
	 */
	private int getNextYear() {
		Calendar cal = monthView.getCalendar();
		cal.add(Calendar.YEAR, 1);
		return cal.get(Calendar.YEAR);
	}

	/**
	 * Sets the current year of the monthView to the given value. Callback for
	 * spinner model.
	 * 
	 * @param value
	 *            the new value of the year.
	 * @return a boolean indicating if a change actually happened.
	 */
	private boolean setYear(Object value) {
		int year = ((Integer) value).intValue();
		Calendar cal = monthView.getCalendar();
		if (cal.get(Calendar.YEAR) == year)
			return false;
		cal.set(Calendar.YEAR, year);
		monthView.setFirstDisplayedDay(cal.getTime());
		return true;
	}

	/**
	 * Thin-layer implementation of a SpinnerModel which is actually backed by
	 * this controller.
	 */
	private class YearSpinnerModel extends AbstractSpinnerModel {

		@Override
		public Object getNextValue() {
			return getNextYear();
		}

		@Override
		public Object getPreviousValue() {
			return getPreviousYear();
		}

		@Override
		public Object getValue() {
			return getYear();
		}

		@Override
		public void setValue(Object value) {
			if (setYear(value)) {
				fireStateChanged();
			}
		}

		@Override
		public void fireStateChanged() {
			super.fireStateChanged();
		}

	}

	private SpinnerModel getYearSpinnerModel() {
		if (yearSpinnerModel == null) {
			yearSpinnerModel = new YearSpinnerModel();
		}
		return yearSpinnerModel;
	}

	/**
	 * The custom header component controlled and configured by this handler.
	 * 
	 */
	protected static class SpinningCalendarHeader extends JXPanel {
		private AbstractButton prevButton;

		private AbstractButton nextButton;

		private JLabel monthText;

		private JSpinner yearSpinner;

		private boolean surroundMonth;

		public SpinningCalendarHeader() {
			initComponents();
		}

		/**
		 * Installs the actions and models to be used by this component.
		 * 
		 * @param prev
		 *            the action to use for the previous button
		 * @param next
		 *            the action to use for the next button
		 * @param model
		 *            the spinner model to use for the spinner.
		 */
		public void setActions(Action prev, Action next, SpinnerModel model) {
			prevButton.setAction(prev);
			nextButton.setAction(next);
			uninstallZoomAction();
			installZoomAction(model);
		}

		/**
		 * Sets the focusable property of the spinner's editor's text field.
		 * 
		 * The default value is false.
		 * 
		 * @param focusable
		 *            the focusable property of the spinner's editor.
		 */
		public void setSpinnerFocusable(boolean focusable) {
			((DefaultEditor) yearSpinner.getEditor()).getTextField().setFocusable(focusable);
		}

		/**
		 * The default value is false.
		 * 
		 * @param surroundMonth
		 */
		public void setArrowsSurroundMonth(boolean surroundMonth) {
			if (this.surroundMonth == surroundMonth)
				return;
			this.surroundMonth = surroundMonth;
			removeAll();
			addComponents();
		}

		/**
		 * Sets the text to use for the month label.
		 * 
		 * @param text
		 *            the text to use for the month label.
		 */
		public void setMonthText(String text) {
			monthText.setText(text);
		}

		/**
		 * {@inheritDoc}
		 * <p>
		 * 
		 * Overridden to set the font of its child components.
		 */
		@Override
		public void setFont(Font font) {
			super.setFont(font);
			if (monthText != null) {
				monthText.setFont(font);
				yearSpinner.setFont(font);
				yearSpinner.getEditor().setFont(font);
				((DefaultEditor) yearSpinner.getEditor()).getTextField().setFont(font);
			}
		}

		/**
		 * {@inheritDoc}
		 * <p>
		 * 
		 * Overridden to set the background of its child compenents.
		 */
		@Override
		public void setBackground(Color bg) {
			super.setBackground(bg);
			for (int i = 0; i < getComponentCount(); i++) {
				getComponent(i).setBackground(bg);
			}
			if (yearSpinner != null) {
				yearSpinner.setBackground(bg);
				yearSpinner.getEditor().setBackground(bg);
				((DefaultEditor) yearSpinner.getEditor()).getTextField().setBackground(bg);
			}
		}

		private void installZoomAction(SpinnerModel model) {
			if (model == null)
				return;
			yearSpinner.setModel(model);
		}

		private void uninstallZoomAction() {
		}

		private void initComponents() {
			createComponents();
			setLayout(new BoxLayout(this, BoxLayout.LINE_AXIS));
			setBorder(BorderFactory.createEmptyBorder(2, 4, 2, 4));
			addComponents();
		}

		/**
         * 
         */
		private void addComponents() {
			if (surroundMonth) {
				add(prevButton);
				add(monthText);
				add(nextButton);
				add(Box.createHorizontalStrut(5));
				add(yearSpinner);
			} else {
				add(prevButton);
				add(Box.createHorizontalGlue());
				add(monthText);
				add(Box.createHorizontalStrut(5));
				add(yearSpinner);
				add(Box.createHorizontalGlue());
				add(nextButton);
			}
		}

		/**
         * 
         */
		private void createComponents() {
			prevButton = createNavigationButton();
			nextButton = createNavigationButton();
			monthText = createMonthText();
			yearSpinner = createSpinner();
		}

		private JLabel createMonthText() {
			JLabel comp = new JLabel() {

				@Override
				public Dimension getMaximumSize() {
					Dimension dim = super.getMaximumSize();
					dim.width = Integer.MAX_VALUE;
					dim.height = Integer.MAX_VALUE;
					return dim;
				}

			};
			comp.setHorizontalAlignment(JLabel.CENTER);
			return comp;
		}

		/**
		 * Creates and returns the JSpinner used for year navigation.
		 * 
		 * @return
		 */
		private JSpinner createSpinner() {
			JSpinner spinner = new JSpinner();
			spinner.setFocusable(false);
			spinner.setBorder(BorderFactory.createEmptyBorder());
			NumberEditor editor = new NumberEditor(spinner);
			editor.getFormat().setGroupingUsed(false);
			editor.getTextField().setFocusable(false);
			spinner.setEditor(editor);
			return spinner;
		}

		private AbstractButton createNavigationButton() {
			JXHyperlink b = new JXHyperlink();
			b.setContentAreaFilled(false);
			b.setBorder(BorderFactory.createEmptyBorder());
			b.setRolloverEnabled(true);
			b.setFocusable(false);
			return b;
		}

	}

}